默认布局:自动路由形成基础菜单结构
管理后台的菜单数据不应该手动维护——它应该直接从 Vue Router 的路由配置中自动生成。这样做的好处是:新增页面只需添加路由配置,菜单会自动更新,避免了路由和菜单两套数据不同步的问题。本节的核心任务是将 RouteRecordRaw 类型转换为 Menu 组件所需的 AppRouteMenuItem 类型。
路由数据到菜单数据的转换
Vue Router 的路由对象(RouteRecordRaw)和菜单组件需要的数据结构(AppRouteMenuItem)并不完全一致。主要差异在于:
| 属性 | RouteRecordRaw | AppRouteMenuItem |
|---|---|---|
| path | 直接包含 | 直接映射 |
| name | RouteRecordName(Symbol 类型) | string |
| meta | RouteMeta | 自定义 RootMeta(含 title、icon、order 等) |
| children | RouteRecordRaw[] | AppRouteMenuItem[] |
| redirect | RouteRecordRedirect(可能是函数) | string(只保留字符串形式) |
generateMenuData 转换函数
// src/layouts/composables/use-layout.ts
function generateMenuData(
routes: RouteRecordRaw[]
): AppRouteMenuItem[] {
const menuData: AppRouteMenuItem[] = []
routes.forEach((route) => {
const menuItem: AppRouteMenuItem = {
path: route.path,
name: route.name as string,
meta: route.meta,
}
// redirect 只保留字符串形式
if (typeof route.redirect === 'string') {
menuItem.redirect = route.redirect
}
// 递归处理子路由
if (
Array.isArray(route.children) &&
route.children.length > 0
) {
menuItem.children = generateMenuData(route.children)
}
menuData.push(menuItem)
})
return menuData
}
typescript
在 Layout 组件中使用 computed 将路由数据转换为菜单数据:
const menus = computed(() => generateMenuData(routes))
typescript
菜单排序:order 字段
路由配置中的 meta.order 字段控制菜单的显示顺序。排序逻辑添加在 useMenu.ts 的 generateMenuKeys 中,紧跟在 filter 之后:
const filteredMenus = menus
.filter((m) => !m.meta?.hideMenu)
.sort((a, b) => {
const orderA = a.meta?.order ?? 100
const orderB = b.meta?.order ?? 100
return orderA - orderB
})
typescript
没有设置 order 的菜单项默认值为 100,排在设置了较小 order 值的菜单项之后。
definePage:页面级路由元信息
在基于文件路由的项目中,使用 definePage 宏为每个页面配置路由元信息:
// src/pages/index.vue
definePage({
meta: {
title: '首页',
icon: 'mdi:home',
},
})
typescript
对于需要隐藏的页面(如 404 页面、重定向中转页):
// src/pages/[...path].vue
definePage({
meta: {
hideMenu: true,
},
})
typescript
同名 .vue 文件的处理
对于路由文件夹(如 pages/components/),需要创建一个同名的 .vue 文件来定义父级菜单的 meta 信息:
src/pages/components/
├── components.vue ← 定义父级 meta(title: '组件示例', icon: 'mdi:apps')
├── components.ts ← 原有的路由配置文件
├── icon.vue ← 子页面
└── notice.vue ← 子页面
text
如果不创建这个同名 .vue 文件,父级菜单会正常显示子菜单,但父级自身的标题会为空。
菜单宽度与样式配置
Layout 组件通过 ThingSetting 接口提供菜单宽度的配置:
interface ThingSetting {
menuWidth?: string | number
// ...其他主题配置
}
typescript
菜单宽度支持两种格式:
// 模板中动态绑定 style
:style="{ width: typeof menuWidth === 'string' ? menuWidth : `${menuWidth}px` }"
typescript
使用 withDefaults 设置默认宽度为 240px:
const props = withDefaults(defineProps<{
settings?: ThingSetting
}>(), {
settings: () => ({
menuWidth: 240,
}),
})
typescript
菜单滚动处理
当菜单项很多超出屏幕高度时,使用 el-scrollbar 包裹 Menu 组件实现滚动:
<aside :style="menuStyle" class="h-full">
<el-scrollbar>
<Menu :data="menus" />
</el-scrollbar>
</aside>
vue
图标样式依赖注入
Menu 组件通过 provide 向子组件(MenuItem、SubMenu)传递图标样式配置:
// menu.vue
provide('iconProps', {
style: { fontSize: '22px' },
class: 'mr-3',
})
typescript
在 MenuItem 中接收:
const iconProps = inject<IconOptions>('iconProps')
typescript
这样所有菜单项的图标大小和间距保持统一,用户只需在 Menu 组件上配置一次。
菜单背景颜色处理
Element Plus 的 background-color 属性已被官方标记为废弃(deprecated),建议使用 bg-color 属性替代。为了兼容两种情况,可以同时设置:
// 菜单项背景透明
style: {
backgroundColor: 'transparent',
'--el-menu-bg-color': 'transparent',
}
typescript
使用 CSS 变量的方式覆盖背景颜色,确保与未来版本兼容。
本节小结
本节实现了从路由配置到菜单数据的自动转换流程:
- 数据转换:
generateMenuData递归遍历路由配置,生成AppRouteMenuItem[]。 - 菜单排序:通过
meta.order字段控制显示顺序,默认值为 100。 - 页面配置:
definePage宏为每个页面配置标题、图标、排序等元信息。 - 宽度适配:支持字符串和数字两种格式的菜单宽度配置。
- 依赖注入:图标样式通过
provide/inject统一管理。
↑